Skip to content

fix(deps): update dependency @modelcontextprotocol/sdk to v1.17.1 #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 14, 2025

Conversation

renovate[bot]
Copy link
Contributor

@renovate renovate bot commented Jul 31, 2025

This PR contains the following updates:

Package Change Age Confidence
@modelcontextprotocol/sdk (source) 1.16.0 -> 1.17.1 age confidence

Release Notes

modelcontextprotocol/typescript-sdk (@​modelcontextprotocol/sdk)

v1.17.1

Compare Source

What's Changed

Full Changelog: modelcontextprotocol/typescript-sdk@1.17.0...1.17.1

v1.17.0

Compare Source

What's Changed

New Contributors 🙏

Full Changelog: modelcontextprotocol/typescript-sdk@1.16.0...1.17.0


Configuration

📅 Schedule: Branch creation - Tuesday through Thursday ( * * * * 2-4 ) (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate bot requested a review from jonathansampson as a code owner July 31, 2025 22:09
@renovate renovate bot force-pushed the renovate/modelcontextprotocol-sdk-1-x branch from f722ae4 to cc7edf1 Compare August 7, 2025 20:27
@renovate renovate bot changed the title fix(deps): update dependency @modelcontextprotocol/sdk to v1.17.0 fix(deps): update dependency @modelcontextprotocol/sdk to v1.17.1 Aug 7, 2025
Copy link
Contributor

github-actions bot commented Aug 7, 2025

[puLL-Merge] - modelcontextprotocol/[email protected]

Diff
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..596e6991d
--- /dev/null
+++ .github/CODEOWNERS
@@ -0,0 +1,11 @@
+# TypeScript SDK Code Owners
+
+# Default owners for everything in the repo
+* @modelcontextprotocol/typescript-sdk
+
+# Auth team owns all auth-related code
+/src/server/auth/ @modelcontextprotocol/typescript-sdk-auth
+/src/client/auth* @modelcontextprotocol/typescript-sdk-auth
+/src/shared/auth* @modelcontextprotocol/typescript-sdk-auth
+/src/examples/client/simpleOAuthClient.ts @modelcontextprotocol/typescript-sdk-auth
+/src/examples/server/demoInMemoryOAuthProvider.ts @modelcontextprotocol/typescript-sdk-auth
\ No newline at end of file
diff --git README.md README.md
index 4684c67c7..f1839845c 100644
--- README.md
+++ README.md
@@ -571,7 +571,6 @@ app.listen(3000);
 
 > [!TIP]
 > When using this in a remote environment, make sure to allow the header parameter `mcp-session-id` in CORS. Otherwise, it may result in a `Bad Request: No valid session ID provided` error. Read the following section for examples.
-> ```
 
 
 #### CORS Configuration for Browser-Based Clients
diff --git package.json package.json
index 1bd2cea91..7bbb0f173 100644
--- package.json
+++ package.json
@@ -1,6 +1,6 @@
 {
   "name": "@modelcontextprotocol/sdk",
-  "version": "1.16.0",
+  "version": "1.17.1",
   "description": "Model Context Protocol implementation for TypeScript",
   "license": "MIT",
   "author": "Anthropic, PBC (https://anthropic.com)",
diff --git src/client/auth.test.ts src/client/auth.test.ts
index b0ea8d1e8..c3049124e 100644
--- src/client/auth.test.ts
+++ src/client/auth.test.ts
@@ -1,6 +1,8 @@
 import { LATEST_PROTOCOL_VERSION } from '../types.js';
 import {
   discoverOAuthMetadata,
+  discoverAuthorizationServerMetadata,
+  buildDiscoveryUrls,
   startAuthorization,
   exchangeAuthorization,
   refreshAuthorization,
@@ -11,7 +13,7 @@ import {
   type OAuthClientProvider,
 } from "./auth.js";
 import {ServerError} from "../server/auth/errors.js";
-import { OAuthMetadata } from '../shared/auth.js';
+import { AuthorizationServerMetadata } from '../shared/auth.js';
 
 // Mock fetch globally
 const mockFetch = jest.fn();
@@ -216,7 +218,7 @@ describe("OAuth Authorization", () => {
         ok: false,
         status: 404,
       });
-      
+
       // Second call (root fallback) succeeds
       mockFetch.mockResolvedValueOnce({
         ok: true,
@@ -226,17 +228,17 @@ describe("OAuth Authorization", () => {
 
       const metadata = await discoverOAuthProtectedResourceMetadata("https://resource.example.com/path/name");
       expect(metadata).toEqual(validMetadata);
-      
+
       const calls = mockFetch.mock.calls;
       expect(calls.length).toBe(2);
-      
+
       // First call should be path-aware
       const [firstUrl, firstOptions] = calls[0];
       expect(firstUrl.toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource/path/name");
       expect(firstOptions.headers).toEqual({
         "MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
       });
-      
+
       // Second call should be root fallback
       const [secondUrl, secondOptions] = calls[1];
       expect(secondUrl.toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");
@@ -251,7 +253,7 @@ describe("OAuth Authorization", () => {
         ok: false,
         status: 404,
       });
-      
+
       // Second call (root fallback) also returns 404
       mockFetch.mockResolvedValueOnce({
         ok: false,
@@ -260,7 +262,7 @@ describe("OAuth Authorization", () => {
 
       await expect(discoverOAuthProtectedResourceMetadata("https://resource.example.com/path/name"))
         .rejects.toThrow("Resource server does not implement OAuth 2.0 Protected Resource Metadata.");
-      
+
       const calls = mockFetch.mock.calls;
       expect(calls.length).toBe(2);
     });
@@ -274,10 +276,10 @@ describe("OAuth Authorization", () => {
 
       await expect(discoverOAuthProtectedResourceMetadata("https://resource.example.com/"))
         .rejects.toThrow("Resource server does not implement OAuth 2.0 Protected Resource Metadata.");
-      
+
       const calls = mockFetch.mock.calls;
       expect(calls.length).toBe(1); // Should not attempt fallback
-      
+
       const [url] = calls[0];
       expect(url.toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");
     });
@@ -291,10 +293,10 @@ describe("OAuth Authorization", () => {
 
       await expect(discoverOAuthProtectedResourceMetadata("https://resource.example.com"))
         .rejects.toThrow("Resource server does not implement OAuth 2.0 Protected Resource Metadata.");
-      
+
       const calls = mockFetch.mock.calls;
       expect(calls.length).toBe(1); // Should not attempt fallback
-      
+
       const [url] = calls[0];
       expect(url.toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");
     });
@@ -302,13 +304,13 @@ describe("OAuth Authorization", () => {
     it("falls back when path-aware discovery encounters CORS error", async () => {
       // First call (path-aware) fails with TypeError (CORS)
       mockFetch.mockImplementationOnce(() => Promise.reject(new TypeError("CORS error")));
-      
+
       // Retry path-aware without headers (simulating CORS retry)
       mockFetch.mockResolvedValueOnce({
         ok: false,
         status: 404,
       });
-      
+
       // Second call (root fallback) succeeds
       mockFetch.mockResolvedValueOnce({
         ok: true,
@@ -318,10 +320,10 @@ describe("OAuth Authorization", () => {
 
       const metadata = await discoverOAuthProtectedResourceMetadata("https://resource.example.com/deep/path");
       expect(metadata).toEqual(validMetadata);
-      
+
       const calls = mockFetch.mock.calls;
       expect(calls.length).toBe(3);
-      
+
       // Final call should be root fallback
       const [lastUrl, lastOptions] = calls[2];
       expect(lastUrl.toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");
@@ -340,10 +342,10 @@ describe("OAuth Authorization", () => {
       await expect(discoverOAuthProtectedResourceMetadata("https://resource.example.com/path", {
         resourceMetadataUrl: "https://custom.example.com/metadata"
       })).rejects.toThrow("Resource server does not implement OAuth 2.0 Protected Resource Metadata.");
-      
+
       const calls = mockFetch.mock.calls;
       expect(calls.length).toBe(1); // Should not attempt fallback when explicit URL is provided
-      
+
       const [url] = calls[0];
       expect(url.toString()).toBe("https://custom.example.com/metadata");
     });
@@ -683,6 +685,222 @@ describe("OAuth Authorization", () => {
     });
   });
 
+  describe("buildDiscoveryUrls", () => {
+    it("generates correct URLs for server without path", () => {
+      const urls = buildDiscoveryUrls("https://auth.example.com");
+
+      expect(urls).toHaveLength(2);
+      expect(urls.map(u => ({ url: u.url.toString(), type: u.type }))).toEqual([
+        {
+          url: "https://auth.example.com/.well-known/oauth-authorization-server",
+          type: "oauth"
+        },
+        {
+          url: "https://auth.example.com/.well-known/openid-configuration",
+          type: "oidc"
+        }
+      ]);
+    });
+
+    it("generates correct URLs for server with path", () => {
+      const urls = buildDiscoveryUrls("https://auth.example.com/tenant1");
+
+      expect(urls).toHaveLength(4);
+      expect(urls.map(u => ({ url: u.url.toString(), type: u.type }))).toEqual([
+        {
+          url: "https://auth.example.com/.well-known/oauth-authorization-server/tenant1",
+          type: "oauth"
+        },
+        {
+          url: "https://auth.example.com/.well-known/oauth-authorization-server",
+          type: "oauth"
+        },
+        {
+          url: "https://auth.example.com/.well-known/openid-configuration/tenant1",
+          type: "oidc"
+        },
+        {
+          url: "https://auth.example.com/tenant1/.well-known/openid-configuration",
+          type: "oidc"
+        }
+      ]);
+    });
+
+    it("handles URL object input", () => {
+      const urls = buildDiscoveryUrls(new URL("https://auth.example.com/tenant1"));
+
+      expect(urls).toHaveLength(4);
+      expect(urls[0].url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/tenant1");
+    });
+  });
+
+  describe("discoverAuthorizationServerMetadata", () => {
+    const validOAuthMetadata = {
+      issuer: "https://auth.example.com",
+      authorization_endpoint: "https://auth.example.com/authorize",
+      token_endpoint: "https://auth.example.com/token",
+      registration_endpoint: "https://auth.example.com/register",
+      response_types_supported: ["code"],
+      code_challenge_methods_supported: ["S256"],
+    };
+
+    const validOpenIdMetadata = {
+      issuer: "https://auth.example.com",
+      authorization_endpoint: "https://auth.example.com/authorize",
+      token_endpoint: "https://auth.example.com/token",
+      jwks_uri: "https://auth.example.com/jwks",
+      subject_types_supported: ["public"],
+      id_token_signing_alg_values_supported: ["RS256"],
+      response_types_supported: ["code"],
+      code_challenge_methods_supported: ["S256"],
+    };
+
+    it("tries URLs in order and returns first successful metadata", async () => {
+      // First OAuth URL fails with 404
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 404,
+      });
+
+      // Second OAuth URL (root) succeeds
+      mockFetch.mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => validOAuthMetadata,
+      });
+
+      const metadata = await discoverAuthorizationServerMetadata(
+        "https://auth.example.com/tenant1"
+      );
+
+      expect(metadata).toEqual(validOAuthMetadata);
+
+      // Verify it tried the URLs in the correct order
+      const calls = mockFetch.mock.calls;
+      expect(calls.length).toBe(2);
+      expect(calls[0][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/tenant1");
+      expect(calls[1][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
+    });
+
+    it("throws error when OIDC provider does not support S256 PKCE", async () => {
+      // OAuth discovery fails
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 404,
+      });
+
+      // OpenID Connect discovery succeeds but without S256 support
+      const invalidOpenIdMetadata = {
+        ...validOpenIdMetadata,
+        code_challenge_methods_supported: ["plain"], // Missing S256
+      };
+
+      mockFetch.mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => invalidOpenIdMetadata,
+      });
+
+      await expect(
+        discoverAuthorizationServerMetadata(
+          "https://auth.example.com"
+        )
+      ).rejects.toThrow("does not support S256 code challenge method required by MCP specification");
+    });
+
+    it("continues on 4xx errors", async () => {
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 400,
+      });
+
+      mockFetch.mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => validOpenIdMetadata,
+      });
+
+      const metadata = await discoverAuthorizationServerMetadata("https://mcp.example.com");
+
+      expect(metadata).toEqual(validOpenIdMetadata);
+
+    });
+
+    it("throws on non-4xx errors", async () => {
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 500,
+      });
+
+      await expect(
+        discoverAuthorizationServerMetadata("https://mcp.example.com")
+      ).rejects.toThrow("HTTP 500");
+    });
+
+    it("handles CORS errors with retry", async () => {
+      // First call fails with CORS
+      mockFetch.mockImplementationOnce(() => Promise.reject(new TypeError("CORS error")));
+
+      // Retry without headers succeeds
+      mockFetch.mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => validOAuthMetadata,
+      });
+
+      const metadata = await discoverAuthorizationServerMetadata(
+        "https://auth.example.com"
+      );
+
+      expect(metadata).toEqual(validOAuthMetadata);
+      const calls = mockFetch.mock.calls;
+      expect(calls.length).toBe(2);
+
+      // First call should have headers
+      expect(calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version");
+
+      // Second call should not have headers (CORS retry)
+      expect(calls[1][1]?.headers).toBeUndefined();
+    });
+
+    it("supports custom fetch function", async () => {
+      const customFetch = jest.fn().mockResolvedValue({
+        ok: true,
+        status: 200,
+        json: async () => validOAuthMetadata,
+      });
+
+      const metadata = await discoverAuthorizationServerMetadata(
+        "https://auth.example.com",
+        { fetchFn: customFetch }
+      );
+
+      expect(metadata).toEqual(validOAuthMetadata);
+      expect(customFetch).toHaveBeenCalledTimes(1);
+      expect(mockFetch).not.toHaveBeenCalled();
+    });
+
+    it("supports custom protocol version", async () => {
+      mockFetch.mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => validOAuthMetadata,
+      });
+
+      const metadata = await discoverAuthorizationServerMetadata(
+        "https://auth.example.com",
+        { protocolVersion: "2025-01-01" }
+      );
+
+      expect(metadata).toEqual(validOAuthMetadata);
+      const calls = mockFetch.mock.calls;
+      const [, options] = calls[0];
+      expect(options.headers).toEqual({
+        "MCP-Protocol-Version": "2025-01-01"
+      });
+    });
+  });
+
   describe("startAuthorization", () => {
     const validMetadata = {
       issuer: "https://auth.example.com",
@@ -909,7 +1127,7 @@ describe("OAuth Authorization", () => {
         authorizationCode: "code123",
         codeVerifier: "verifier123",
         redirectUri: "http://localhost:3000/callback",
-        addClientAuthentication: (headers: Headers, params: URLSearchParams, url: string | URL, metadata: OAuthMetadata) => {
+        addClientAuthentication: (headers: Headers, params: URLSearchParams, url: string | URL, metadata: AuthorizationServerMetadata) => {
           headers.set("Authorization", "Basic " + btoa(validClientInfo.client_id + ":" + validClientInfo.client_secret));
           params.set("example_url", typeof url === 'string' ? url : url.toString());
           params.set("example_metadata", metadata.authorization_endpoint);
@@ -1091,7 +1309,7 @@ describe("OAuth Authorization", () => {
         metadata: validMetadata,
         clientInformation: validClientInfo,
         refreshToken: "refresh123",
-        addClientAuthentication: (headers: Headers, params: URLSearchParams, url: string | URL, metadata?: OAuthMetadata) => {
+        addClientAuthentication: (headers: Headers, params: URLSearchParams, url: string | URL, metadata?: AuthorizationServerMetadata) => {
           headers.set("Authorization", "Basic " + btoa(validClientInfo.client_id + ":" + validClientInfo.client_secret));
           params.set("example_url", typeof url === 'string' ? url : url.toString());
           params.set("example_metadata", metadata?.authorization_endpoint ?? '?');
@@ -1919,17 +2137,17 @@ describe("OAuth Authorization", () => {
 
       // Verify the correct URLs were fetched
       const calls = mockFetch.mock.calls;
-      
+
       // First call should be to PRM
       expect(calls[0][0].toString()).toBe("https://my.resource.com/.well-known/oauth-protected-resource/path/name");
-      
+
       // Second call should be to AS metadata with the path from authorization server
       expect(calls[1][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/oauth");
     });
 
     it("supports overriding the fetch function used for requests", async () => {
       const customFetch = jest.fn();
-      
+
       // Mock PRM discovery
       customFetch.mockResolvedValueOnce({
         ok: true,
@@ -1939,7 +2157,7 @@ describe("OAuth Authorization", () => {
           authorization_servers: ["https://auth.example.com"],
         }),
       });
-      
+
       // Mock AS metadata discovery
       customFetch.mockResolvedValueOnce({
         ok: true,
@@ -1956,7 +2174,7 @@ describe("OAuth Authorization", () => {
 
       const mockProvider: OAuthClientProvider = {
         get redirectUrl() { return "http://localhost:3000/callback"; },
-        get clientMetadata() { 
+        get clientMetadata() {
           return {
             client_name: "Test Client",
             redirect_uris: ["http://localhost:3000/callback"],
@@ -1981,10 +2199,10 @@ describe("OAuth Authorization", () => {
       expect(result).toBe("REDIRECT");
       expect(customFetch).toHaveBeenCalledTimes(2);
       expect(mockFetch).not.toHaveBeenCalled();
-      
+
       // Verify custom fetch was called for PRM discovery
       expect(customFetch.mock.calls[0][0].toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");
-      
+
       // Verify custom fetch was called for AS metadata discovery
       expect(customFetch.mock.calls[1][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
     });
diff --git src/client/auth.ts src/client/auth.ts
index b5a3a6a43..56826045a 100644
--- src/client/auth.ts
+++ src/client/auth.ts
@@ -7,7 +7,9 @@ import {
   OAuthMetadata,
   OAuthClientInformationFull,
   OAuthProtectedResourceMetadata,
-  OAuthErrorResponseSchema
+  OAuthErrorResponseSchema,
+  AuthorizationServerMetadata,
+  OpenIdProviderDiscoveryMetadataSchema
 } from "../shared/auth.js";
 import { OAuthClientInformationFullSchema, OAuthMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthTokensSchema } from "../shared/auth.js";
 import { checkResourceAllowed, resourceUrlFromServerUrl } from "../shared/auth-utils.js";
@@ -108,7 +110,7 @@ export interface OAuthClientProvider {
    * @param url - The token endpoint URL being called
    * @param metadata - Optional OAuth metadata for the server, which may include supported authentication methods
    */
-  addClientAuthentication?(headers: Headers, params: URLSearchParams, url: string | URL, metadata?: OAuthMetadata): void | Promise<void>;
+  addClientAuthentication?(headers: Headers, params: URLSearchParams, url: string | URL, metadata?: AuthorizationServerMetadata): void | Promise<void>;
 
   /**
    * If defined, overrides the selection and validation of the
@@ -319,7 +321,7 @@ async function authInternal(
 ): Promise<AuthResult> {
 
   let resourceMetadata: OAuthProtectedResourceMetadata | undefined;
-  let authorizationServerUrl = serverUrl;
+  let authorizationServerUrl: string | URL | undefined;
   try {
     resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl }, fetchFn);
     if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
@@ -329,11 +331,19 @@ async function authInternal(
     // Ignore errors and fall back to /.well-known/oauth-authorization-server
   }
 
+  /**
+   * If we don't get a valid authorization server metadata from protected resource metadata,
+   * fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
+   */
+  if (!authorizationServerUrl) {
+    authorizationServerUrl = serverUrl;
+  }
+
   const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata);
 
-  const metadata = await discoverOAuthMetadata(serverUrl, {
-    authorizationServerUrl
-  }, fetchFn);
+  const metadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, {
+    fetchFn,
+  });
 
   // Handle client registration if needed
   let clientInformation = await Promise.resolve(provider.clientInformation());
@@ -524,15 +534,21 @@ async function fetchWithCorsRetry(
 }
 
 /**
- * Constructs the well-known path for OAuth metadata discovery
+ * Constructs the well-known path for auth-related metadata discovery
  */
-function buildWellKnownPath(wellKnownPrefix: string, pathname: string): string {
-  let wellKnownPath = `/.well-known/${wellKnownPrefix}${pathname}`;
+function buildWellKnownPath(
+  wellKnownPrefix: 'oauth-authorization-server' | 'oauth-protected-resource' | 'openid-configuration',
+  pathname: string = '',
+  options: { prependPathname?: boolean } = {}
+): string {
+  // Strip trailing slash from pathname to avoid double slashes
   if (pathname.endsWith('/')) {
-    // Strip trailing slash from pathname to avoid double slashes
-    wellKnownPath = wellKnownPath.slice(0, -1);
+    pathname = pathname.slice(0, -1);
   }
-  return wellKnownPath;
+
+  return options.prependPathname
+    ? `${pathname}/.well-known/${wellKnownPrefix}`
+    : `/.well-known/${wellKnownPrefix}${pathname}`;
 }
 
 /**
@@ -594,6 +610,8 @@ async function discoverMetadataWithFallback(
  *
  * If the server returns a 404 for the well-known endpoint, this function will
  * return `undefined`. Any other errors will be thrown as exceptions.
+ *
+ * @deprecated This function is deprecated in favor of `discoverAuthorizationServerMetadata`.
  */
 export async function discoverOAuthMetadata(
   issuer: string | URL,
@@ -615,7 +633,7 @@ export async function discoverOAuthMetadata(
   if (typeof authorizationServerUrl === 'string') {
     authorizationServerUrl = new URL(authorizationServerUrl);
   }
-  protocolVersion ??= LATEST_PROTOCOL_VERSION;
+  protocolVersion ??= LATEST_PROTOCOL_VERSION ;
 
   const response = await discoverMetadataWithFallback(
     authorizationServerUrl,
@@ -640,6 +658,137 @@ export async function discoverOAuthMetadata(
   return OAuthMetadataSchema.parse(await response.json());
 }
 
+
+/**
+ * Builds a list of discovery URLs to try for authorization server metadata.
+ * URLs are returned in priority order:
+ * 1. OAuth metadata at the given URL
+ * 2. OAuth metadata at root (if URL has path)
+ * 3. OIDC metadata endpoints
+ */
+export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url: URL; type: 'oauth' | 'oidc' }[] {
+  const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
+  const hasPath = url.pathname !== '/';
+  const urlsToTry: { url: URL; type: 'oauth' | 'oidc' }[] = [];
+
+
+  if (!hasPath) {
+    // Root path: https://example.com/.well-known/oauth-authorization-server
+    urlsToTry.push({
+      url: new URL('/.well-known/oauth-authorization-server', url.origin),
+      type: 'oauth'
+    });
+
+    // OIDC: https://example.com/.well-known/openid-configuration
+    urlsToTry.push({
+      url: new URL(`/.well-known/openid-configuration`, url.origin),
+      type: 'oidc'
+    });
+
+    return urlsToTry;
+  }
+
+  // Strip trailing slash from pathname to avoid double slashes
+  let pathname = url.pathname;
+  if (pathname.endsWith('/')) {
+    pathname = pathname.slice(0, -1);
+  }
+
+  // 1. OAuth metadata at the given URL
+  // Insert well-known before the path: https://example.com/.well-known/oauth-authorization-server/tenant1
+  urlsToTry.push({
+    url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
+    type: 'oauth'
+  });
+
+  // Root path: https://example.com/.well-known/oauth-authorization-server
+  urlsToTry.push({
+    url: new URL('/.well-known/oauth-authorization-server', url.origin),
+    type: 'oauth'
+  });
+
+  // 3. OIDC metadata endpoints
+  // RFC 8414 style: Insert /.well-known/openid-configuration before the path
+  urlsToTry.push({
+    url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
+    type: 'oidc'
+  });
+  // OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path
+  urlsToTry.push({
+    url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
+    type: 'oidc'
+  });
+
+  return urlsToTry;
+}
+
+/**
+ * Discovers authorization server metadata with support for RFC 8414 OAuth 2.0 Authorization Server Metadata
+ * and OpenID Connect Discovery 1.0 specifications.
+ *
+ * This function implements a fallback strategy for authorization server discovery:
+ * 1. Attempts RFC 8414 OAuth metadata discovery first
+ * 2. If OAuth discovery fails, falls back to OpenID Connect Discovery
+ *
+ * @param authorizationServerUrl - The authorization server URL obtained from the MCP Server's
+ *                                 protected resource metadata, or the MCP server's URL if the
+ *                                 metadata was not found.
+ * @param options - Configuration options
+ * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
+ * @param options.protocolVersion - MCP protocol version to use, defaults to LATEST_PROTOCOL_VERSION
+ * @returns Promise resolving to authorization server metadata, or undefined if discovery fails
+ */
+export async function discoverAuthorizationServerMetadata(
+  authorizationServerUrl: string | URL,
+  {
+    fetchFn = fetch,
+    protocolVersion = LATEST_PROTOCOL_VERSION,
+  }: {
+    fetchFn?: FetchLike;
+    protocolVersion?: string;
+  } = {}
+): Promise<AuthorizationServerMetadata | undefined> {
+  const headers = { 'MCP-Protocol-Version': protocolVersion };
+
+  // Get the list of URLs to try
+  const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
+
+  // Try each URL in order
+  for (const { url: endpointUrl, type } of urlsToTry) {
+    const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
+
+    if (!response) {
+      throw new Error(`CORS error trying to load ${type === 'oauth' ? 'OAuth' : 'OpenID provider'} metadata from ${endpointUrl}`);
+    }
+
+    if (!response.ok) {
+      // Continue looking for any 4xx response code.
+      if (response.status >= 400 && response.status < 500) {
+        continue; // Try next URL
+      }
+      throw new Error(`HTTP ${response.status} trying to load ${type === 'oauth' ? 'OAuth' : 'OpenID provider'} metadata from ${endpointUrl}`);
+    }
+
+    // Parse and validate based on type
+    if (type === 'oauth') {
+      return OAuthMetadataSchema.parse(await response.json());
+    } else {
+      const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
+
+      // MCP spec requires OIDC providers to support S256 PKCE
+      if (!metadata.code_challenge_methods_supported?.includes('S256')) {
+        throw new Error(
+          `Incompatible OIDC provider at ${endpointUrl}: does not support S256 code challenge method required by MCP specification`
+        );
+      }
+
+      return metadata;
+    }
+  }
+
+  return undefined;
+}
+
 /**
  * Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
  */
@@ -653,7 +802,7 @@ export async function startAuthorization(
     state,
     resource,
   }: {
-    metadata?: OAuthMetadata;
+    metadata?: AuthorizationServerMetadata;
     clientInformation: OAuthClientInformation;
     redirectUrl: string | URL;
     scope?: string;
@@ -746,7 +895,7 @@ export async function exchangeAuthorization(
     addClientAuthentication,
     fetchFn,
   }: {
-    metadata?: OAuthMetadata;
+    metadata?: AuthorizationServerMetadata;
     clientInformation: OAuthClientInformation;
     authorizationCode: string;
     codeVerifier: string;
@@ -774,6 +923,7 @@ export async function exchangeAuthorization(
   // Exchange code for tokens
   const headers = new Headers({
     "Content-Type": "application/x-www-form-urlencoded",
+    "Accept": "application/json",
   });
   const params = new URLSearchParams({
     grant_type: grantType,
@@ -831,7 +981,7 @@ export async function refreshAuthorization(
     addClientAuthentication,
     fetchFn,
   }: {
-    metadata?: OAuthMetadata;
+    metadata?: AuthorizationServerMetadata;
     clientInformation: OAuthClientInformation;
     refreshToken: string;
     resource?: URL;
@@ -902,7 +1052,7 @@ export async function registerClient(
     clientMetadata,
     fetchFn,
   }: {
-    metadata?: OAuthMetadata;
+    metadata?: AuthorizationServerMetadata;
     clientMetadata: OAuthClientMetadata;
     fetchFn?: FetchLike;
   },
diff --git src/client/sse.test.ts src/client/sse.test.ts
index 24bfe094c..4fce9976f 100644
--- src/client/sse.test.ts
+++ src/client/sse.test.ts
@@ -352,6 +352,11 @@ describe("SSEClientTransport", () => {
   });
 
   describe("auth handling", () => {
+    const authServerMetadataUrls = [
+      "/.well-known/oauth-authorization-server",
+      "/.well-known/openid-configuration",
+    ];
+
     let mockAuthProvider: jest.Mocked<OAuthClientProvider>;
 
     beforeEach(() => {
@@ -608,7 +613,7 @@ describe("SSEClientTransport", () => {
       authServer.close();
 
       authServer = createServer((req, res) => {
-        if (req.url === "/.well-known/oauth-authorization-server") {
+        if (req.url && authServerMetadataUrls.includes(req.url)) {
           res.writeHead(404).end();
           return;
         }
@@ -730,7 +735,7 @@ describe("SSEClientTransport", () => {
       authServer.close();
 
       authServer = createServer((req, res) => {
-        if (req.url === "/.well-known/oauth-authorization-server") {
+        if (req.url && authServerMetadataUrls.includes(req.url)) {
           res.writeHead(404).end();
           return;
         }
@@ -875,7 +880,7 @@ describe("SSEClientTransport", () => {
       authServer.close();
 
       authServer = createServer((req, res) => {
-        if (req.url === "/.well-known/oauth-authorization-server") {
+        if (req.url && authServerMetadataUrls.includes(req.url)) {
           res.writeHead(404).end();
           return;
         }
diff --git src/client/streamableHttp.ts src/client/streamableHttp.ts
index 77a15c923..12714ea44 100644
--- src/client/streamableHttp.ts
+++ src/client/streamableHttp.ts
@@ -207,7 +207,7 @@ export class StreamableHTTPClientTransport implements Transport {
         headers.set("last-event-id", resumptionToken);
       }
 
-const response = await (this._fetch ?? fetch)(this._url, {
+      const response = await (this._fetch ?? fetch)(this._url, {
         method: "GET",
         headers,
         signal: this._abortController?.signal,
@@ -427,7 +427,7 @@ const response = await (this._fetch ?? fetch)(this._url, {
         signal: this._abortController?.signal,
       };
 
-const response = await (this._fetch ?? fetch)(this._url, init);
+      const response = await (this._fetch ?? fetch)(this._url, init);
 
       // Handle session ID received during initialization
       const sessionId = response.headers.get("mcp-session-id");
@@ -533,7 +533,7 @@ const response = await (this._fetch ?? fetch)(this._url, init);
         signal: this._abortController?.signal,
       };
 
-const response = await (this._fetch ?? fetch)(this._url, init);
+      const response = await (this._fetch ?? fetch)(this._url, init);
 
       // We specifically handle 405 as a valid response according to the spec,
       // meaning the server does not support explicit session termination
diff --git src/shared/auth.ts src/shared/auth.ts
index 467680a56..47eba9ac5 100644
--- src/shared/auth.ts
+++ src/shared/auth.ts
@@ -56,6 +56,68 @@ export const OAuthMetadataSchema = z
   })
   .passthrough();
 
+/**
+ * OpenID Connect Discovery 1.0 Provider Metadata
+ * see: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
+ */
+export const OpenIdProviderMetadataSchema = z
+  .object({
+    issuer: z.string(),
+    authorization_endpoint: z.string(),
+    token_endpoint: z.string(),
+    userinfo_endpoint: z.string().optional(),
+    jwks_uri: z.string(),
+    registration_endpoint: z.string().optional(),
+    scopes_supported: z.array(z.string()).optional(),
+    response_types_supported: z.array(z.string()),
+    response_modes_supported: z.array(z.string()).optional(),
+    grant_types_supported: z.array(z.string()).optional(),
+    acr_values_supported: z.array(z.string()).optional(),
+    subject_types_supported: z.array(z.string()),
+    id_token_signing_alg_values_supported: z.array(z.string()),
+    id_token_encryption_alg_values_supported: z.array(z.string()).optional(),
+    id_token_encryption_enc_values_supported: z.array(z.string()).optional(),
+    userinfo_signing_alg_values_supported: z.array(z.string()).optional(),
+    userinfo_encryption_alg_values_supported: z.array(z.string()).optional(),
+    userinfo_encryption_enc_values_supported: z.array(z.string()).optional(),
+    request_object_signing_alg_values_supported: z.array(z.string()).optional(),
+    request_object_encryption_alg_values_supported: z
+      .array(z.string())
+      .optional(),
+    request_object_encryption_enc_values_supported: z
+      .array(z.string())
+      .optional(),
+    token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
+    token_endpoint_auth_signing_alg_values_supported: z
+      .array(z.string())
+      .optional(),
+    display_values_supported: z.array(z.string()).optional(),
+    claim_types_supported: z.array(z.string()).optional(),
+    claims_supported: z.array(z.string()).optional(),
+    service_documentation: z.string().optional(),
+    claims_locales_supported: z.array(z.string()).optional(),
+    ui_locales_supported: z.array(z.string()).optional(),
+    claims_parameter_supported: z.boolean().optional(),
+    request_parameter_supported: z.boolean().optional(),
+    request_uri_parameter_supported: z.boolean().optional(),
+    require_request_uri_registration: z.boolean().optional(),
+    op_policy_uri: z.string().optional(),
+    op_tos_uri: z.string().optional(),
+  })
+  .passthrough();
+
+/**
+ * OpenID Connect Discovery metadata that may include OAuth 2.0 fields
+ * This schema represents the real-world scenario where OIDC providers
+ * return a mix of OpenID Connect and OAuth 2.0 metadata fields
+ */
+export const OpenIdProviderDiscoveryMetadataSchema =
+  OpenIdProviderMetadataSchema.merge(
+    OAuthMetadataSchema.pick({
+      code_challenge_methods_supported: true,
+    })
+  );
+
 /**
  * OAuth 2.1 token response
  */
@@ -133,8 +195,10 @@ export const OAuthTokenRevocationRequestSchema = z.object({
   token_type_hint: z.string().optional(),
 }).strip();
 
-
 export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
+export type OpenIdProviderMetadata = z.infer<typeof OpenIdProviderMetadataSchema>;
+export type OpenIdProviderDiscoveryMetadata = z.infer<typeof OpenIdProviderDiscoveryMetadataSchema>;
+
 export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
 export type OAuthErrorResponse = z.infer<typeof OAuthErrorResponseSchema>;
 export type OAuthClientMetadata = z.infer<typeof OAuthClientMetadataSchema>;
@@ -143,3 +207,6 @@ export type OAuthClientInformationFull = z.infer<typeof OAuthClientInformationFu
 export type OAuthClientRegistrationError = z.infer<typeof OAuthClientRegistrationErrorSchema>;
 export type OAuthTokenRevocationRequest = z.infer<typeof OAuthTokenRevocationRequestSchema>;
 export type OAuthProtectedResourceMetadata = z.infer<typeof OAuthProtectedResourceMetadataSchema>;
+
+// Unified type for authorization server metadata
+export type AuthorizationServerMetadata = OAuthMetadata | OpenIdProviderDiscoveryMetadata;
diff --git a/src/shared/protocol-transport-handling.test.ts b/src/shared/protocol-transport-handling.test.ts
new file mode 100644
index 000000000..3baa9b638
--- /dev/null
+++ src/shared/protocol-transport-handling.test.ts
@@ -0,0 +1,189 @@
+import { describe, expect, test, beforeEach } from "@jest/globals";
+import { Protocol } from "./protocol.js";
+import { Transport } from "./transport.js";
+import { Request, Notification, Result, JSONRPCMessage } from "../types.js";
+import { z } from "zod";
+
+// Mock Transport class
+class MockTransport implements Transport {
+  id: string;
+  onclose?: () => void;
+  onerror?: (error: Error) => void;
+  onmessage?: (message: unknown) => void;
+  sentMessages: JSONRPCMessage[] = [];
+
+  constructor(id: string) {
+    this.id = id;
+  }
+
+  async start(): Promise<void> {}
+  
+  async close(): Promise<void> {
+    this.onclose?.();
+  }
+  
+  async send(message: JSONRPCMessage): Promise<void> {
+    this.sentMessages.push(message);
+  }
+}
+
+describe("Protocol transport handling bug", () => {
+  let protocol: Protocol<Request, Notification, Result>;
+  let transportA: MockTransport;
+  let transportB: MockTransport;
+
+  beforeEach(() => {
+    protocol = new (class extends Protocol<Request, Notification, Result> {
+      protected assertCapabilityForMethod(): void {}
+      protected assertNotificationCapability(): void {}
+      protected assertRequestHandlerCapability(): 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;
+    });
+
+    const TestRequestSchema = z.object({
+      method: z.literal("test/method"),
+      params: z.object({
+        from: z.string()
+      }).optional()
+    });
+
+    protocol.setRequestHandler(
+      TestRequestSchema,
+      async (request) => {
+        console.log(`Processing request from ${request.params?.from}`);
+        return handlerPromise;
+      }
+    );
+
+    // Client A connects and sends a request
+    await protocol.connect(transportA);
+    
+    const requestFromA = {
+      jsonrpc: "2.0" as const,
+      method: "test/method",
+      params: { from: "clientA" },
+      id: 1
+    };
+    
+    // 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);
+    
+    const requestFromB = {
+      jsonrpc: "2.0" as const,
+      method: "test/method", 
+      params: { from: "clientB" },
+      id: 2
+    };
+    
+    // Client B sends its own request
+    transportB.onmessage?.(requestFromB);
+    
+    // Now complete A's request
+    resolveHandler!({ data: "responseForA" } as Result);
+    
+    // Wait for async operations to complete
+    await new Promise(resolve => setTimeout(resolve, 10));
+    
+    // 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
+    
+    // Transport A should receive response for request ID 1
+    expect(transportA.sentMessages.length).toBe(1);
+    expect(transportA.sentMessages[0]).toMatchObject({
+      jsonrpc: "2.0",
+      id: 1,
+      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
+    });
+  });
+
+  test("demonstrates the timing issue with multiple rapid connections", async () => {
+    const delays: number[] = [];
+    const results: { transport: string; response: JSONRPCMessage[] }[] = [];
+    
+    const DelayedRequestSchema = z.object({
+      method: z.literal("test/delayed"),
+      params: z.object({
+        delay: z.number(),
+        client: z.string()
+      }).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;
+      }
+    );
+
+    // Rapid succession of connections and requests
+    await protocol.connect(transportA);
+    transportA.onmessage?.({
+      jsonrpc: "2.0" as const,
+      method: "test/delayed",
+      params: { delay: 50, client: "A" },
+      id: 1
+    });
+
+    // Connect B while A is processing
+    setTimeout(async () => {
+      await protocol.connect(transportB);
+      transportB.onmessage?.({
+        jsonrpc: "2.0" as const,
+        method: "test/delayed", 
+        params: { delay: 10, client: "B" },
+        id: 2
+      });
+    }, 10);
+
+    // 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
+    expect(transportA.sentMessages.length).toBe(1);
+    expect(transportB.sentMessages.length).toBe(1);
+  });
+});
\ No newline at end of file
diff --git src/shared/protocol.ts src/shared/protocol.ts
index 6142140dd..7df190ba1 100644
--- src/shared/protocol.ts
+++ src/shared/protocol.ts
@@ -217,7 +217,10 @@ export abstract class Protocol<
   /**
    * A handler to invoke for any request types that do not have their own handler installed.
    */
-  fallbackRequestHandler?: (request: Request) => Promise<SendResultT>;
+  fallbackRequestHandler?: (
+    request: JSONRPCRequest,
+    extra: RequestHandlerExtra<SendRequestT, SendNotificationT>
+  ) => Promise<SendResultT>;
 
   /**
    * A handler to invoke for any notification types that do not have their own handler installed.
@@ -367,8 +370,11 @@ export abstract class Protocol<
     const handler =
       this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
 
+    // Capture the current transport at request time to ensure responses go to the correct client
+    const capturedTransport = this._transport;
+
     if (handler === undefined) {
-      this._transport
+      capturedTransport
         ?.send({
           jsonrpc: "2.0",
           id: request.id,
@@ -390,7 +396,7 @@ export abstract class Protocol<
 
     const fullExtra: RequestHandlerExtra<SendRequestT, SendNotificationT> = {
       signal: abortController.signal,
-      sessionId: this._transport?.sessionId,
+      sessionId: capturedTransport?.sessionId,
       _meta: request.params?._meta,
       sendNotification:
         (notification) =>
@@ -411,7 +417,7 @@ export abstract class Protocol<
             return;
           }
 
-          return this._transport?.send({
+          return capturedTransport?.send({
             result,
             jsonrpc: "2.0",
             id: request.id,
@@ -422,7 +428,7 @@ export abstract class Protocol<
             return;
           }
 
-          return this._transport?.send({
+          return capturedTransport?.send({
             jsonrpc: "2.0",
             id: request.id,
             error: {
diff --git src/types.ts src/types.ts
index b96ab0500..323e37389 100644
--- src/types.ts
+++ src/types.ts
@@ -458,11 +458,31 @@ export const TextResourceContentsSchema = ResourceContentsSchema.extend({
   text: z.string(),
 });
 
+
+/**
+ * A Zod schema for validating Base64 strings that is more performant and
+ * robust for very large inputs than the default regex-based check. It avoids
+ * stack overflows by using the native `atob` function for validation.
+ */
+const Base64Schema = z.string().refine(
+    (val) => {
+        try {
+            // atob throws a DOMException if the string contains characters
+            // that are not part of the Base64 character set.
+            atob(val);
+            return true;
+        } catch {
+            return false;
+        }
+    },
+    { message: "Invalid Base64 string" },
+);
+
 export const BlobResourceContentsSchema = ResourceContentsSchema.extend({
   /**
    * A base64-encoded string representing the binary data of the item.
    */
-  blob: z.string().base64(),
+  blob: Base64Schema,
 });
 
 /**
@@ -718,7 +738,7 @@ export const ImageContentSchema = z
     /**
      * The base64-encoded image data.
      */
-    data: z.string().base64(),
+    data: Base64Schema,
     /**
      * The MIME type of the image. Different providers may support different image types.
      */
@@ -741,7 +761,7 @@ export const AudioContentSchema = z
     /**
      * The base64-encoded audio data.
      */
-    data: z.string().base64(),
+    data: Base64Schema,
     /**
      * The MIME type of the audio. Different providers may support different audio types.
      */
@@ -894,7 +914,7 @@ export const ToolSchema = BaseMetadataSchema.extend({
     })
     .passthrough(),
   /**
-   * An optional JSON Schema object defining the structure of the tool's output returned in 
+   * An optional JSON Schema object defining the structure of the tool's output returned in
    * the structuredContent field of a CallToolResult.
    */
   outputSchema: z.optional(

Description

This PR implements several significant changes to the MCP TypeScript SDK:

  1. Enhanced OAuth/OIDC Discovery: Adds support for OpenID Connect Discovery 1.0 alongside the existing OAuth 2.0 Authorization Server Metadata (RFC 8414) support, with a fallback strategy that tries multiple discovery endpoints.

  2. Protocol Transport Handling Fix: Fixes a critical bug where responses could be sent to the wrong transport when multiple clients are connected simultaneously by capturing the transport reference at request time.

  3. Base64 Validation Performance: Replaces regex-based Base64 validation with a more performant atob-based validation to prevent stack overflows on large inputs.

  4. Version Bump: Updates package version from 1.16.0 to 1.17.1.

  5. Code Maintenance: Adds CODEOWNERS file, fixes documentation formatting, and includes comprehensive test coverage.

Security Hotspots

  1. Base64 Validation Changes (Medium Risk): The new Base64Schema uses atob() for validation instead of regex, which could potentially have different security characteristics. While atob() is generally safer and prevents ReDoS attacks, it's important to ensure it handles malicious inputs appropriately across all environments.

  2. OAuth Discovery URL Construction (Low Risk): The buildDiscoveryUrls function constructs URLs by manipulating pathnames, which could potentially be vulnerable to URL injection if the input URLs are not properly validated, though the function appears to handle this correctly.

Privacy Hotspots

  1. Auth Server Discovery (Medium Risk): The enhanced OAuth/OIDC discovery process now tries multiple endpoints and may expose more information about the authorization server setup through error messages and discovery attempts.
Changes

Changes

.github/CODEOWNERS

  • Added: New CODEOWNERS file defining code ownership for the TypeScript SDK, with special ownership rules for auth-related code.

README.md

  • Fixed: Removed stray code fence marker in documentation.

package.json

  • Updated: Version bumped from 1.16.0 to 1.17.1.

src/client/auth.test.ts

  • Added: Comprehensive test coverage for new discovery functions including buildDiscoveryUrls and discoverAuthorizationServerMetadata.
  • Updated: Modified existing tests to use new AuthorizationServerMetadata type instead of OAuthMetadata.

src/client/auth.ts

  • Added: New functions buildDiscoveryUrls and discoverAuthorizationServerMetadata implementing OAuth/OIDC discovery with fallback strategy.
  • Updated: Modified auth flow to use new discovery mechanism and updated type signatures to use AuthorizationServerMetadata.
  • Deprecated: Marked discoverOAuthMetadata as deprecated in favor of the new discovery method.

src/client/sse.test.ts & src/client/streamableHttp.ts

  • Fixed: Indentation issues and updated test expectations for new auth server discovery endpoints.

src/shared/auth.ts

  • Added: New Zod schemas for OpenID Connect Discovery metadata (OpenIdProviderMetadataSchema, OpenIdProviderDiscoveryMetadataSchema).
  • Added: New unified type AuthorizationServerMetadata combining OAuth and OIDC metadata.

src/shared/protocol-transport-handling.test.ts

  • Added: New comprehensive test file demonstrating and testing the fix for the transport handling bug.

src/shared/protocol.ts

  • Fixed: Critical bug where responses could be sent to wrong transport by capturing transport reference at request time instead of using the current transport reference.
  • Updated: Modified fallback request handler signature to include extra parameters.

src/types.ts

  • Added: New Base64Schema using atob() for more performant Base64 validation.
  • Updated: Replaced regex-based Base64 validation in BlobResourceContentsSchema, ImageContentSchema, and AudioContentSchema.
sequenceDiagram
    participant Client as MCP Client
    participant Protocol as Protocol
    participant Transport as Transport
    participant AuthServer as Auth Server
    
    Client->>Protocol: connect(transport)
    Protocol->>Transport: start()
    
    Client->>Protocol: auth request
    Protocol->>AuthServer: discover OAuth metadata
    
    alt OAuth Discovery Success
        AuthServer-->>Protocol: OAuth metadata
    else OAuth Discovery Fails
        Protocol->>AuthServer: discover OIDC metadata
        AuthServer-->>Protocol: OIDC metadata
    end
    
    Protocol->>Protocol: capture transport at request time
    Protocol->>AuthServer: start authorization
    AuthServer-->>Protocol: auth response
    
    Protocol->>Transport: send response (to captured transport)
    Transport-->>Client: response delivered to correct client
Loading

@jonathansampson jonathansampson merged commit 0e44d80 into main Aug 14, 2025
5 checks passed
@jonathansampson jonathansampson deleted the renovate/modelcontextprotocol-sdk-1-x branch August 14, 2025 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant