Repository: modelcontextprotocol/typescript-sdk (TypeScript)

Issue:
Empty string for `tos_uri` and `logo_uri` causing validation errors during DCR as of 1.17.4
**Describe the bug**
Previously, if a client did not send a `tos_uri` or `logo_uri` in their Dynamic Client Registration request, clients using the MCP SDK accepted empty strings in the response of the registration call.

With https://github.com/modelcontextprotocol/typescript-sdk/pull/877 released in https://github.com/modelcontextprotocol/typescript-sdk/releases/tag/1.17.5, empty strings are treated as "unsafe" in these fields, and cause DCR to fail.

**To Reproduce**
This can be replicated with the newest MCP inspector and the ActiveCampaign MCP server.

Steps to reproduce the behavior:
1. Create a free trial ActiveCampaign account: https://www.activecampaign.com/free
2. Start the latest MCP inspector (or any version >= 0.16.6, when this upstream change was pulled in)
3. Try to connect to the ActiveCampaign MCP server
4. See an error instead of successful DCR

<img width="3394" height="1014" alt="Image" src="https://github.com/user-attachments/assets/ab647909-0dda-4c54-8aeb-8bd418c0d3c5" />

**Expected behavior**
The client registers a dynamic client successfully

**Logs**
Example error response:

```json
 [ { "validation": "url", "code": "invalid_string", "message": "Invalid url", "path": [ "logo_uri" ] }, { "code": "custom", "message": "URL must be parseable", "fatal": true, "path": [ "logo_uri" ] }, { "validation": "url", "code": "invalid_string", "message": "Invalid url", "path": [ "tos_uri" ] }, { "code": "custom", "message": "URL must be parseable", "fatal": true, "path": [ "tos_uri" ] } ]
```

**Additional context**
I don't see any indication of why empty strings are being treated as potentially harmful in these cases - the issue and PR that initiated these changes were to make sure that authentication URLs did not try to execute javascript - https://github.com/modelcontextprotocol/typescript-sdk/pull/841



Discussion:
We're running into the same issue with ActiveCampaign's MCP.  We're not yet on 1.17.4 as a client and observing this bug.

Based on my debugging I see two possible paths:

- The schema validation should permit empty strings (as they were permitted before the change)

- If it is intended / a breaking change that these empty strings are no longer permitted, it should be documented somewhere that the response from client registration should leave the fields unset instead of empty.  In this case, the ActiveCampaign MCP should not return empty strings for these fields in their registration response, because IIUC the ClientInformation schema will be echoed back in a subsequent request, which will fail validation on a server at 1.17.4.

Somewhat strangely, claude code also runs into this bug, but claude.ai does not; so claude.ai must be processing the registration responses specially to make this work; we're considering doing the same.

an empty string should be considered safe by the zod validation, agree this is a bug.
From https://github.com/modelcontextprotocol/typescript-sdk/pull/987#pullrequestreview-3299475864

> should we really be accepting an empty string? Should the endpoints just not be returning anything for this property, which is already allowed by the .optional()?



Patch:
diff --git a/src/shared/auth.ts b/src/shared/auth.ts
index 0e079646b..c5ddbda16 100644
--- a/src/shared/auth.ts
+++ b/src/shared/auth.ts
@@ -151,6 +151,11 @@ export const OAuthErrorResponseSchema = z.object({
     error_uri: z.string().optional()
 });
 
+/**
+ * Optional version of SafeUrlSchema that allows empty string for retrocompatibility on tos_uri and logo_uri
+ */
+export const OptionalSafeUrlSchema = SafeUrlSchema.optional().or(z.literal('').transform(() => undefined));
+
 /**
  * RFC 7591 OAuth 2.0 Dynamic Client Registration metadata
  */
@@ -162,10 +167,10 @@ export const OAuthClientMetadataSchema = z
         response_types: z.array(z.string()).optional(),
         client_name: z.string().optional(),
         client_uri: SafeUrlSchema.optional(),
-        logo_uri: SafeUrlSchema.optional(),
+        logo_uri: OptionalSafeUrlSchema,
         scope: z.string().optional(),
         contacts: z.array(z.string()).optional(),
-        tos_uri: SafeUrlSchema.optional(),
+        tos_uri: OptionalSafeUrlSchema,
         policy_uri: z.string().optional(),
         jwks_uri: SafeUrlSchema.optional(),
         jwks: z.any().optional(),


Test patch:
diff --git a/src/shared/auth.test.ts b/src/shared/auth.test.ts
index 41f9dc1a9..71877f341 100644
--- a/src/shared/auth.test.ts
+++ b/src/shared/auth.test.ts
@@ -1,5 +1,11 @@
 import { describe, it, expect } from '@jest/globals';
-import { SafeUrlSchema, OAuthMetadataSchema, OpenIdProviderMetadataSchema, OAuthClientMetadataSchema } from './auth.js';
+import {
+    SafeUrlSchema,
+    OAuthMetadataSchema,
+    OpenIdProviderMetadataSchema,
+    OAuthClientMetadataSchema,
+    OptionalSafeUrlSchema
+} from './auth.js';
 
 describe('SafeUrlSchema', () => {
     it('accepts valid HTTPS URLs', () => {
@@ -26,6 +32,12 @@ describe('SafeUrlSchema', () => {
     });
 });
 
+describe('OptionalSafeUrlSchema', () => {
+    it('accepts empty string and transforms it to undefined', () => {
+        expect(OptionalSafeUrlSchema.parse('')).toBe(undefined);
+    });
+});
+
 describe('OAuthMetadataSchema', () => {
     it('validates complete OAuth metadata', () => {
         const metadata = {